/*
 * Machine.java
 *
 * Stateroom - Java framework library for finite state machines
 * Copyright (C) 2009 by Matthew Werny
 * All Rights Reserved
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */
package net.iceflow.stateroom;

import java.util.HashMap;
import java.util.Map;

import net.iceflow.stateroom.enums.Status;
import net.iceflow.stateroom.exception.DefinitionFailedException;
import net.iceflow.stateroom.exception.DuplicateStateException;
import net.iceflow.stateroom.exception.EndStateNotFoundException;
import net.iceflow.stateroom.exception.InvalidContextException;
import net.iceflow.stateroom.exception.StartStateNotFoundException;
import net.iceflow.stateroom.exception.UnknownStateException;
import net.iceflow.stateroom.interfaces.Condition;
import net.iceflow.stateroom.interfaces.MachineDefinitionControls;
import net.iceflow.stateroom.interfaces.MachineStrategy;
import net.iceflow.stateroom.interfaces.Model;
import net.iceflow.stateroom.interfaces.State;

/**
 * Finite state machine container to allow the running of machine strategies.
 * @param <M> Type used to represent the machine model
 */
public final class Machine<M extends Model> {

   /** Machine strategy instance used to define machine instance states, transitions and initialize contexts. */
   private final MachineStrategy<M> _strategy;

   /** Association of the unique state to its transition container instance. */
   private Map<State<M>, StateTransitionsContainer<M>> _stateTransitionsContainer = new HashMap<State<M>, StateTransitionsContainer<M>>();

   /**
    * Constructs a new Machine based on the definition in the provided MachineStrategy.
    * @param strategy Machine strategy used to define the machine and initialize contexts
    * @throws DefinitionFailedException If the strategy failed to define the machine
    */
   public Machine(final MachineStrategy<M> strategy) throws DefinitionFailedException {
      // Validate that the arguement is not null
      Utility.validateNotNull(strategy, "strategy may not be null");

      // Set the strategy
      this._strategy = strategy;

      try {
         // Cause the machine to be defined
         this._strategy.defineMachine(new Controls());
      } catch (UnknownStateException ex) {
         // Throw an exception to indicate that an unknow state was referenced when defining the machine
         throw new DefinitionFailedException("Unknown state error: " + ex.getMessage(), ex);
      } catch (DuplicateStateException ex) {
         // Throw an exception to indicate that a duplicate state was attempted to be added when defining the machine
         throw new DefinitionFailedException("Duplicate state error: " + ex.getMessage(), ex);
      }
   }

   /**
    * Adds a state to the machine.
    * @param state State to be added
    * @param acceptState Whether the state being added is an accept state
    */
   private void addState(final State<M> state, final boolean acceptState) throws DuplicateStateException {
      // If the state instance is not already among the states added to the machine...
      if (this._stateTransitionsContainer.containsKey(state)) {
         // Throw an exception to indicate that a duplicate was attempted to be added
         throw new DuplicateStateException("Duplicate state may not be added.");
      } else {
         // Create a new container containing the state
         StateTransitionsContainer<M> container = new StateTransitionsContainer<M>(state, acceptState);

         // Add the container to the map
         this._stateTransitionsContainer.put(state, container);
      }
   }

   /**
    * Adds a mono-directional transition between two states that is followed, if the condition indicates true, when it is evaluated.
    * @param start Start state from which the transition originates
    * @param end End state to which the transition terminates
    * @param condition Sufficient condition used to determine if the transition should be followed at the time of evaluation
    * @throws UnknownStateException Thrown if either the start or end state do not exist in the set of states in the machine
    */
   private void addTransition(State<M> start, State<M> end, Condition<M> condition) throws UnknownStateException {
      // If the start state does not exist in the set of states...
      if (this._stateTransitionsContainer.containsKey(start) == false) {
         // ...immediately throw an exception to indicate the problem.
         throw new StartStateNotFoundException("Starting Vertex Not Found");
      }

      // If the end state does not exist in the set of states...
      if (this._stateTransitionsContainer.containsKey(end) == false) {
         // ...immediately throw an exception to indicate the problem.
         throw new EndStateNotFoundException("Ending Vertex Not Found");
      }

      // Extract the transition container specific to the start state
      StateTransitionsContainer<M> transitionContainer = this._stateTransitionsContainer.get(start);

      // Add the transition
      transitionContainer.addTransition(end, condition);
   }

   /**
    * Evaluates a context until it either crashes or enters an accept state.
    * <p>
    * WARNING: If your machine strategy defines a machine that is Turing complete, or is otherwise capable of looping forever,
    * this method is also capable of looping forever and not returning. Approach usage of this method with caution.
    * </p>
    * @param context Context to evaluate
    * @throws InvalidContextException If the provided context model is determined to be invalid
    */
   public void evaluateUntilHalted(final Context<M> context) throws InvalidContextException {
      // As long as the machine hasn't crashed, or accepted, continue to evaluate it
      while (context.getStatus() == Status.READY) {
         // Cause the machine to perform a single evaluation
         this.evaluate(context);
      }
   }

   /**
    * Evaluates a context for one evaluation cycle, performs the appropriate state update and returns.
    * @param context Context to evaluate
    * @throws InvalidContextException If the provided context model is determined to be invalid
    */
   public void evaluate(final Context<M> context) throws InvalidContextException {
      // Assume that the next state is null, until it can be determined
      State<M> nextState = null;

      // Determine the next state
      State<M> currentState = context.getCurrentState();

      // If the state in the machine is not known by the machine...
      if (!this._stateTransitionsContainer.containsKey(currentState)) {
         // ...immediately throw up an exception to inform the caller.
         throw new InvalidContextException("Context contains a reference to an unknown state.");
      }

      // Extract the container that contains the current state
      StateTransitionsContainer<M> currentStateContainer = this._stateTransitionsContainer.get(currentState);

      // If the model indicates that the machine is in an end state
      if (context.getModel().isEnd()) {
         // If the machine the is also in an accept state
         if (currentStateContainer.isAcceptState()) {
            // Set the status to accepted, indicating that the input word was valid
            context.setStatus(Status.ACCEPTED);
         } else {
            // Set the status to crashed, indicating that the input word was invalid
            context.setStatus(Status.CRASHED);
         }
      } else {
         // Attempt to resolve the next state
         nextState = currentStateContainer.resolveNextState(context);

         // If the next state was actually resolved
         if (nextState != null) {
            // Cause the old state's transition out handler to be called
            currentState.stateTransitionedOut(context);

            // Switch to the next state
            context.setCurrentState(nextState);

            // Cause the new state's transition in handler to be called
            nextState.stateTransitionedIn(context);
         }
      }
   }

   /**
    * Creates a newly initialized context representing a specific machine instance.
    * @return Newly create context
    */
   public Context<M> newContext() {
      // Create the new context
      Context<M> context = new Context<M>();

      // Populate the context with any machine-specific objects
      this._strategy.populateContext(context);

      // Cause the context to be populated with its start state
      this._strategy.populateStartState(context);

      // Initialize the context's status to ready
      context.setStatus(Status.READY);

      // Return the newly created and initialized context
      return context;
   }

   /** Implementation of MachineDefinitionControls to allow access to specific private methods under controlled circumstances. */
   private class Controls implements MachineDefinitionControls<M> {

      /**
       * Proxies calls through to the private addState method.
       * @param state State to add
       * @param acceptState Whether the state is an accept state
       */
      @Override
      public void addState(State<M> state, boolean acceptState) throws DuplicateStateException {
         // Proxy the request through to the machine's protected addState method
         Machine.this.addState(state, acceptState);
      }

      /**
       * Proxies calls through to the private addTransition method.
       * @param start Start state
       * @param end End state
       * @param condition Condition instance used to evaluate whether a transition should be followed
       * @throws UnknownStateException If an unknown state is referenced
       */
      @Override
      public void addTransition(State<M> start, State<M> end, Condition<M> condition) throws UnknownStateException {
         // Proxy the request through to the machine's protected addTransition method
         Machine.this.addTransition(start, end, condition);
      }
   }
}
